# utils.py
#
# Copyright (c) 2006 Michael Hobbs
#
# This file is part of iTorrent.
#
# iTorrent is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# iTorrent is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with iTorrent; if not, write to the Free Software
# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA


import os
import re
import md5
import urllib
import sys
import inspect
import time

import version
import FeedStore


PARTIALS_DIR = 'partials'
TORRENTS_DIR = 'torrents'
ENCLOSURE_PATH = '/enclosure/'
TORRENT_PATH = '/torrent/'
DEBUG_DUMP = 'debug.dump'

URL_HOST_RE = re.compile(r'^.+?://(.+?)(:\d+)?(/.*|$)')
URL_BASE_RE = re.compile(r'^.+/(.+?)([?&]|$)')

UNSEEN_DELETE_DAYS = 30
SEEN_DELETE_DAYS = 30



def getPartialPathName(url, pathName=None):
	if not os.path.isdir(PARTIALS_DIR):
		os.makedirs(PARTIALS_DIR)
	return HashedPath(url).partialPath(pathName)

def getTorrentPathName(url, pathName):
	if not os.path.isdir(TORRENTS_DIR):
		os.makedirs(TORRENTS_DIR)
	return HashedPath(url).torrentPath(pathName)

def getEnclosureURL(url, host):
	return HashedPath(url).enclosureURL(host)

def getTorrentURL(url, host, fileName):
	return HashedPath(url).torrentURL(host, fileName)


class HashedPath(object):
	def __init__(self, url):
		super(HashedPath, self).__init__()
		self.url = url
		# Ensure that we avoid name collisions by including md5 hash of url in paths.
		self.hash = md5.new(url).hexdigest()
		self.hashLen = len(self.hash)

	def truncPathName(self, dirName, pathName, extra=0):
		# Add one for directory separator, and another one for hash separator
		pathLen = len(dirName) + 1 + self.hashLen + 1 + extra
		# Limit path to less than 256 chars, by truncating the front of the file name.
		return pathName[-(255 - pathLen):]

	def truncUrlName(self, dirName, extra=0):
		# Get the file name from the url.
		match = URL_BASE_RE.match(self.url)
		# TODO: return an error if no match
		return self.truncPathName(dirName, match.group(1), extra)

	def torrentPath(self, pathName):
		suffix = '.torrent'
		fileName = self.truncPathName(TORRENTS_DIR, pathName, len(suffix))
		return os.path.join(TORRENTS_DIR, fileName + '-' + self.hash + suffix)

	def partialPath(self, pathName=None):
		if not pathName:
			pathName = self.truncUrlName(PARTIALS_DIR)
		else:
			pathName = self.truncPathName(PARTIALS_DIR, pathName)
		return os.path.join(PARTIALS_DIR, pathName + '-' + self.hash)

	def torrentURL(self, host, fileName):
		fileName = self.truncPathName(TORRENT_PATH, urllib.quote(fileName))
		return 'http://%s%s%s/%s' % (host, TORRENT_PATH, self.hash, fileName)

	def enclosureURL(self, host):
		fileName = self.truncUrlName(ENCLOSURE_PATH)
		return 'http://%s%s%s/%s' % (host, ENCLOSURE_PATH, self.hash, fileName)


# TODO: make this a class or something with different methods, rather than all these strange arguments
def getHashedPath(url, pathOrRegex, dirName, suffix, separator='-', hashFirst=False):
	if hasattr(pathOrRegex, 'match'):
		# Get the file name from the url.
		match = pathOrRegex.match(url)
		# TODO: return an error if no match
		pathName = match.group(1)
	else:
		pathName = pathOrRegex
	# Ensure that we avoid name collisions by prefixing name with md5 hash of url.
	hash = md5.new(url).hexdigest()
	tempPath = os.path.join(dirName, separator + hash + suffix)
	# Limit path to less than 256 chars, by truncating the front of the file name.
	pathName = pathName[-(255 - len(tempPath)):]
	if hashFirst:
		return os.path.join(dirName, hash + separator + pathName + suffix)
	return os.path.join(dirName, pathName + separator + hash + suffix)

def debugDump():
	f = file(DEBUG_DUMP, 'wb')
	type, value, traceback = sys.exc_info()
	f.write('%s\r\n%s\r\n\r\n' % (type, value))
	if not traceback:
		frame = inspect.currentframe()
	else:
		top = traceback
		while top.tb_next:
			top = top.tb_next
		frame = top.tb_frame
	while frame:
		writeFrame(frame, f)
		frame = frame.f_back
	if traceback:
		f.write(('-' * 80) + '\r\n')
		frame = traceback.tb_frame
		while frame:
			writeFrame(frame, f)
			frame = frame.f_back
		# end while
	f.close()

def writeFrame(frame, f, prefix = '#'):
	fileName, line, funcName, ctxt, _ = inspect.getframeinfo(frame, 1)
	if not ctxt:
		ctxt = ['<UNKNOWN>']
	f.write(prefix + ('#' * 40) + '\r\n')
	f.write('%s function: %s\r\n' % (prefix, funcName))
	f.write('%s file name: %s\r\n' % (prefix, fileName))
	f.write('%s line number: %d\r\n' % (prefix, line))
	f.write(prefix + ('#' * 40) + '\r\n')
	f.write(prefix + ' code:\r\n')
	f.write('%s    %s\r\n' % (prefix, ctxt[0].strip()))
	f.write(prefix + ('#' * 40) + '\r\n')
	f.write(prefix + ' locals:\r\n')
	for name, value in frame.f_locals.items():
		if name == 'self':
			f.write(prefix + '    self:\r\n')
			for name, value in inspect.getmembers(value):
				if not inspect.ismethod(value) and not name.startswith('__'):
					f.write('%s%s%s = %s\r\n' % (prefix, ' ' * 8, name, value))
				# end if
			# end for
		else:
			if not inspect.isfunction(value) and not inspect.ismodule(value) and not name.startswith('__'):
				f.write('%s%s%s = %r\r\n' % (prefix, ' ' * 8, name, value))
			# end if
		# end if
	f.write(prefix + ('#' * 40) + '\r\n')
	f.write('\r\n')

def addBitTorrentToSysPath():
	try:
		import BitTorrent
	except ImportError:
		sys.path.append(getBTDir())
	# end try

def getBTDir():
	myDir = sys.path[0]
	if hasattr(version, 'BT_VERSION'):
		btVersion = version.BT_VERSION
	else:
		versionRE = re.compile(r'^BitTorrent-(\d+)\.(\d+)\.(\d+)$')
		maxVersion = [0, 0, 0]
		for fileName in os.listdir(myDir):
			if not os.path.isdir(os.path.join(myDir, fileName)):
				continue
			match = versionRE.match(fileName)
			if not match:
				continue
			btVersion = [int(number) for number in match.groups()]
			if isHigherVersion(btVersion, maxVersion):
				maxVersion = btVersion
			# end if
		btVersion = maxVersion
	return os.path.join(myDir, 'BitTorrent-%d.%d.%d' % tuple(btVersion))

def isHigherVersion(a, b):
	if a[0] != b[0]:
		return (a[0] > b[0])
	if a[1] != b[1]:
		return (a[1] > b[1])
	return (a[2] > b[2])

def remove(path):
	if not os.path.isdir(path):
		os.remove(path)
		return
	for root, dirs, files in os.walk(path, topdown=False):
		[os.remove(os.path.join(root, name)) for name in files]
		[os.rmdir(os.path.join(root, name)) for name in dirs]
	os.rmdir(path)

# TODO: refactor these tidy functions
def tidyDataDir(dataDir, feedStore, rawServer):
	# metainfo dir and routing_table file stay pretty clean. Clear out resume dir,
	# though.
	validEntries = ['metainfo', 'routing_table', 'resume', TORRENTS_DIR, PARTIALS_DIR, FeedStore.FILE_NAME, DEBUG_DUMP]
	[remove(os.path.join(dataDir, e)) for e in os.listdir(dataDir) if e not in validEntries]
	resumeDir = os.path.join(dataDir, 'resume')
	[remove(os.path.join(resumeDir, e)) for e in os.listdir(resumeDir)]
	tidyTorrentsDir(dataDir, feedStore)
	tidyPartialsDir(dataDir, feedStore)
	# Re-tidy every hour
	rawServer.add_task(tidyDataDir, 60 * 60, (dataDir, feedStore, rawServer))

def tidyTorrentsDir(dataDir, feedStore):
	validPaths = {}
	for url in feedStore.keys():
		record = feedStore[url]
		torrentFile = record.torrentFile
		if torrentFile:
			torrentFile = os.path.join(dataDir, torrentFile)
			if isUnseen(record):
				del feedStore[url]
			elif not isOldPath(torrentFile):
				validPaths[torrentFile] = None
			# end if
		# end if
	torrentsDir = os.path.join(dataDir, TORRENTS_DIR)
	for entry in os.listdir(torrentsDir):
		path = os.path.join(torrentsDir, entry)
		if path not in validPaths:
			remove(path)
		# end if
	# end for

def tidyPartialsDir(dataDir, feedStore):
	validPaths = {}
	for url in feedStore.keys():
		record = feedStore[url]
		partialPath = record.partialPath
		if partialPath:
			partialPath = os.path.join(dataDir, partialPath)
			if isUnseen(record):
				del feedStore[url]
			elif not isOldPath(partialPath):
				validPaths[partialPath] = None
			# end if
		# end if
	partialsDir = os.path.join(dataDir, PARTIALS_DIR)
	for entry in os.listdir(partialsDir):
		path = os.path.join(partialsDir, entry)
		if path not in validPaths:
			remove(path)
		# end if
	# end for

def isUnseen(record):
	# Delete files for feeds that haven't been updated after UNSEEN_DELETE_DAYS of
	# inactivity.
	return record.lastSeen and time.time() - (UNSEEN_DELETE_DAYS * 24 * 60 * 60) > record.lastSeen

def isOldPath(path):
	# Delete files for feeds that have been updated, but never downloaded after
	# SEEN_DELETE_DAYS of inactivity.
	if not os.path.exists(path):
		return False
	return time.time() - (SEEN_DELETE_DAYS * 24 * 60 * 60) > os.path.getmtime(path)
